const path = require("path");
const fs = require("fs");
const readline = require("readline");
const { getIgnoreList, convertToUTF16LE } = require("./utils");
const { getDirTreeText } = require("./tree");
const {
  ROOT_DIR,
  PROJECT_NAME,
  TODAY,
  MEDIA_FILE_FORMAT,
  EXTRA_IGNORE_FILE,
} = require("./config");

// 根据用户指定的项目名和当前日期，生成一个新的输出文件名
const fileName = `${PROJECT_NAME}_${TODAY}.txt`;
/**
 * 读取指定文件的所有内容，并以字符串形式返回
 * @param {String} filePath 读取指定文件路径的文件内容
 * @returns 文件内容
 */
async function readFileContent(filePath) {
  try {
    // 使用 fs 创建可读流
    const fileStream = fs.createReadStream(filePath);
    // 使用 readline 按行读取文件内容
    const rl = readline.createInterface({
      input: fileStream,
      // crlfDelay 可以保证不同操作系统的换行符被正确识别
      crlfDelay: Infinity,
    });
    const lines = [];
    // 对于每一行，将其加入到数组 lines 中
    rl.on("line", (line) => {
      lines.push(line);
    });
    // 等待文件读取完成后，将所有行的内容拼接起来作为函数返回值
    return new Promise((resolve) => {
      rl.on("close", () => {
        // 内容前后添加```，使其在markdown中显示为代码块
        lines.unshift("```");
        lines.push("```");
        resolve(lines.join("\n"));
      });
    });
  } catch (err) {
    console.error(`Error reading file ${filePath}`, err);
  }
}

/**
 * 遍历指定目录下的所有文件和子目录，去除 .gitignore 中指定的忽略文件，将剩下的文件读取内容后作为一个二元组数组返回，每个二元组包含文件路径和文件内容
 * @param {String} rootDir 读取指定目录下的所有文件内容
 * @param {String} ignoreList 忽略的文件列表
 * @param {Function} progressCallback 读取进度回调函数
 * @returns 二元组数组
 */
async function readDir(rootDir, ignoreList, progressCallback) {
  let fileContentArr = [];
  const files = await fs.promises.readdir(rootDir);
  let fileIndex = 0;
  // 逐个读取目录下的所有子文件和子目录
  for (const file of files) {
    const filePath = path.join(rootDir, file);

    /**
     * 如果是媒体文件，则不管它，继续遍历下一个 file
     */
    const isMediaFile = MEDIA_FILE_FORMAT.some((format) =>
      filePath.endsWith(format)
    );
    /**
     * 如果忽略列表中包含了该文件，则不管它，继续遍历下一个 file
     * 此处判断需要对地址中*号进行判断，如果有*号，需要将*号替换为正则表达式中的.*，再进行判断
     */
    const isIgnoreFile = ignoreList.some((rule) => {
      if (typeof rule == "function") {
        return rule(filePath);
      }
      if (rule.includes("*")) {
        const reg = new RegExp(rule.replace(/\*/g, ".*"));
        return reg.test(filePath);
      }
      return rule === filePath;
    });

    /**
     * 是否排除该文件
     */
    let isExcludeFile = false;
    if (isIgnoreFile || isMediaFile) {
      isExcludeFile = true;
    }

    const stats = await fs.promises.stat(filePath);
    const isDirectory = stats.isDirectory();
    if (isDirectory && !isExcludeFile) {
      // 递归读取该目录下的文件, 并将结果数组与当前结果数组合并
      const subDirFiles = await readDir(filePath, ignoreList, progressCallback);
      fileContentArr = [...fileContentArr, ...subDirFiles];
    } else {
      const isDirectoryText = {
        true: "忽略 - 目录",
        false: "忽略 - 文件内容",
      }[isDirectory.toString()];
      // 读取文件内容，并将其存入结果数组中. 如果是忽略文件，则将文件内容置为空字符串
      const fileContent = isExcludeFile
        ? isDirectoryText
        : await readFileContent(filePath);
      fileContentArr.push([filePath, fileContent]);
    }
    // 如果传入的进度回调函数存在，则将当前进度以及当前文件在结果数组中的索引传递给回调函数
    if (progressCallback && fileIndex % 10 == 0) {
      const progress = Math.floor(((fileIndex + 1) / files.length) * 100);
      progressCallback(progress, filePath);
    }
    fileIndex++;
  }
  return fileContentArr;
}

async function main() {
  const ignoreList = await getIgnoreList(EXTRA_IGNORE_FILE);

  // 读取指定目录下的所有文件内容，并将其存储在二元组数组 fileContentArr 中
  let currentProgress = 0;
  const fileContentArr = await readDir(
    ROOT_DIR,
    ignoreList,
    (progress, filePath) => {
      if (progress > currentProgress) {
        currentProgress = progress;
        readline.clearLine(process.stdout, 0);
        readline.cursorTo(process.stdout, 0, null);
        const progressBar =
          "[" + "#".repeat(progress / 5) + " ".repeat(20 - progress / 5) + "]";
        process.stdout.write(
          `读取进度：${progress}% || ${progressBar}\r ${filePath}\r`
        );
      }
    }
  );

  // 拼接输出文件的完整路径
  const filepath = path.join(__dirname, `../output/${fileName}`);
  // 检查输出文件是否存在，如果不存在就创建该文件
  if (!fs.existsSync(filepath)) {
    fs.openSync(filepath, "w");
  }

  // 创建一个写入流，写入模式为追加，将内容写入到输出文件中
  const ws = fs.createWriteStream(filepath, { flags: "a" });
  let fileContentStr = "";
  // 写入文件头部信息
  fileContentStr += `项目名称：${PROJECT_NAME}\n项目根目录：${ROOT_DIR}\n\n`;
  // ws.write(`项目名称：${PROJECT_NAME}\n项目根目录：${ROOT_DIR}\n\n`);
  // 写入tree树结构
  fileContentStr += "项目结构：\n";
  // ws.write("项目结构：\n");
  const dirTreeText = await getDirTreeText();
  fileContentStr += dirTreeText;
  // ws.write(dirTreeText);
  fileContentStr += "\n\n";
  // ws.write("\n\n");
  // 遍历二元组数组 fileContentArr，向输出文件中写入每个文件的相对路径和内容
  fileContentArr.forEach(([filePath, fileContent]) => {
    const relativePath = path.relative(ROOT_DIR, filePath);
    fileContentStr += `\n文件相对地址：${relativePath}\n文件内容：\n${fileContent}\n\n`;
    // ws.write(`\n文件相对地址：${relativePath}\n文件内容：\n${fileContent}\n\n`);
  });
  // 将所有内容转为utf-16 le写入到输出文件中
  ws.write(convertToUTF16LE(fileContentStr));

  // 关闭写入流
  ws.close();

  // 输出读取完成的信息
  process.stdout.write(
    `\n读取完成！共读取 ${fileContentArr.length} 个文件。\n`
  );
}

main().catch((err) => {
  console.error("Unexpected error", err);
  process.exit(1);
});
